use egui::NumExt;
use rand::random;
use std::{
    any::Any,
    collections::HashMap,
    fmt::Debug,
    hash::Hash,
    sync::{
        mpsc::{Receiver, Sender},
        Arc,
    },
};
use thunderdome::Index;
use QcCore::scene_system::scene::Scene;

use crate::{components::dock::DockView, core::context::Context};

pub trait ItemTrait {
    fn new(index: Index) -> Self;

    fn get_id(&self) -> Index;

    fn get_egui_id(&self) -> egui::Id;

    fn get_string(&self, context: Arc<Context>) -> String;
}

#[derive(Debug)]
enum Item<T> {
    Container(Vec<T>),
    Leaf(String),
}

#[derive(Debug)]
pub enum Command<T> {
    /// Set the selected item
    SetSelectedItem(Option<T>),

    /// Move the currently dragged item to the given container and position.
    MoveItem {
        moved_item_id: T,
        target_container_id: T,
        target_position_index: usize,
    },

    /// Specify the currently identified target container to be highlighted.
    HighlightTargetContainer(T),
}

#[derive(Debug)]
pub struct HierarchicalDragAndDrop<T> {
    /// All items
    items: HashMap<T, Item<T>>,

    /// Id of the root item (not displayed in the UI)
    pub root_id: T,

    /// Selected item, if any
    pub selected_item: Option<T>,

    /// If a drag is ongoing, this is the id of the destination container (if any was identified)
    ///
    /// This is used to highlight the target container.
    pub target_container: Option<T>,

    /// Channel to receive commands from the UI
    command_receiver: Receiver<Command<T>>,

    /// Channel to send commands from the UI
    command_sender: Sender<Command<T>>,

    context: Arc<Context>,

    name: String,
}

//
// Data stuff
//
impl<T> HierarchicalDragAndDrop<T>
where
    T: Default + Debug + Clone + Copy + Eq + Hash + ItemTrait,
{
    pub fn new(context: Arc<Context>) -> Self {
        let root_item = Item::Container(Vec::new());
        let root_id = T::default();

        let (command_sender, command_receiver) = std::sync::mpsc::channel();

        let name = format!("ID{}", random::<i32>());
        let res = Self {
            items: std::iter::once((root_id, root_item)).collect(),
            root_id,
            selected_item: None,
            target_container: None,
            command_receiver,
            command_sender,
            context,
            name,
        };
        res
    }
    fn container(&self, id: T) -> Option<&Vec<T>> {
        match self.items.get(&id) {
            Some(Item::Container(children)) => Some(&children),
            _ => None,
        }
    }

    /// Does some container contain the given item?
    ///
    /// Used to test if a target location is suitable for a given dragged item.
    fn contains(&self, container_id: T, item_id: T) -> bool {
        if let Some(children) = self.container(container_id) {
            if container_id == item_id {
                return true;
            }

            if children.contains(&item_id) {
                return true;
            }

            for child_id in children {
                if self.contains(*child_id, item_id) {
                    return true;
                }
            }

            return false;
        }

        false
    }

    /// Move item `item_id` to `container_id` at position `pos`.
    fn move_item(&mut self, item_id: T, container_id: T, mut pos: usize) {
        println!("Moving {item_id:?} to {container_id:?} at position {pos:?}");

        // Remove the item from its current location. Note: we must adjust the target position if the item is
        // moved within the same container, as the removal might shift the positions by one.
        if let Some((source_parent_id, source_pos)) = self.parent_and_pos(item_id) {
            if let Some(Item::Container(children)) = self.items.get_mut(&source_parent_id) {
                children.remove(source_pos);
            }

            if source_parent_id == container_id && source_pos < pos {
                pos -= 1;
            }
        }

        if let Some(Item::Container(children)) = self.items.get_mut(&container_id) {
            children.insert(pos.at_most(children.len()), item_id);
        }
    }

    /// Find the parent of an item, and the index of that item within the parent's children.
    fn parent_and_pos(&self, id: T) -> Option<(T, usize)> {
        if id == self.root_id {
            None
        } else {
            self.parent_and_pos_impl(id, self.root_id)
        }
    }

    fn parent_and_pos_impl(&self, id: T, container_id: T) -> Option<(T, usize)> {
        if let Some(children) = self.container(container_id) {
            for (idx, child_id) in children.iter().enumerate() {
                if child_id == &id {
                    return Some((container_id, idx));
                } else if self.container(*child_id).is_some() {
                    let res = self.parent_and_pos_impl(id, *child_id);
                    if res.is_some() {
                        return res;
                    }
                }
            }
        }

        None
    }

    pub fn add_container(&mut self, parent_id: T, index: Index) -> T {
        let id = T::new(index);
        let item = Item::Container(Vec::new());

        self.items.insert(id, item);

        if let Some(Item::Container(children)) = self.items.get_mut(&parent_id) {
            children.push(id);
        }

        id
    }

    pub fn add_container_item(&mut self, parent_id: T, id: T) -> T {
        let item = Item::Container(Vec::new());

        self.items.insert(id, item);

        if let Some(Item::Container(children)) = self.items.get_mut(&parent_id) {
            children.push(id);
        }

        id
    }

    pub fn add_leaf(&mut self, parent_id: T, text: &str, index: Index) {
        let id = T::new(index);
        let item = Item::Leaf(text.to_string());

        self.items.insert(id, item);

        if let Some(Item::Container(children)) = self.items.get_mut(&parent_id) {
            children.push(id);
        }
    }

    fn send_command(&self, command: Command<T>) {
        // The only way this can fail is if the receiver has been dropped.
        self.command_sender.send(command).ok();
    }
}

//
// UI stuff
//
impl<T> HierarchicalDragAndDrop<T>
where
    T: Default + Debug + Clone + Copy + Eq + Hash + ItemTrait + Send + Sync + Any,
{
    pub fn ui(&mut self, ui: &mut egui::Ui, func: impl Fn(&Command<T>) -> bool) {
        if let Some(top_level_items) = self.container(self.root_id) {
            self.container_children_ui(ui, top_level_items);
        }

        // deselect by clicking in the empty space
        if ui
            .interact(
                ui.available_rect_before_wrap(),
                self.name.clone().into(),
                egui::Sense::click(),
            )
            .clicked()
        {
            self.send_command(Command::SetSelectedItem(None));
        }

        // always reset the target container
        self.target_container = None;

        while let Ok(command) = self.command_receiver.try_recv() {
            println!("Received command: {command:?}");

            let is_move = func(&command);
            match command {
                Command::SetSelectedItem(item_id) => {
                    self.selected_item = item_id;
                }

                Command::MoveItem {
                    moved_item_id,
                    target_container_id,
                    target_position_index,
                } => {
                    if is_move {
                        self.move_item(moved_item_id, target_container_id, target_position_index);
                    }
                }

                Command::HighlightTargetContainer(item_id) => {
                    self.target_container = Some(item_id);
                }
            }
        }
    }

    fn container_ui(&self, ui: &mut egui::Ui, item_id: T, children: &Vec<T>) {
        let (response, head_response, body_resp) =
            egui::collapsing_header::CollapsingState::load_with_default_open(
                ui.ctx(),
                item_id.get_egui_id(),
                true,
            )
            .show_header(ui, |ui| {
                let name = item_id.get_string(self.context.clone());
                let text = egui::RichText::new(format!("{}", name)).size(14.);
                ui.add(
                    egui::Label::new(text)
                        .selectable(false)
                        .sense(egui::Sense::click_and_drag()),
                )
            })
            .body(|ui| {
                self.container_children_ui(ui, children);
            });

        if head_response.inner.clicked() {
            self.send_command(Command::SetSelectedItem(Some(item_id)));
        }

        // if self.target_container == Some(item_id) {
        //     ui.painter().rect_stroke(
        //         head_response.inner.rect,
        //         2.0,
        //         (1.0, ui.visuals().selection.bg_fill),
        //     );
        // }

        self.handle_drag_and_drop_interaction(
            ui,
            item_id,
            true,
            &head_response.inner.union(response),
            body_resp.as_ref().map(|r| &r.response),
        );
    }

    fn container_children_ui(&self, ui: &mut egui::Ui, children: &Vec<T>) {
        for child_id in children {
            // check if the item is selected
            ui.visuals_mut().override_text_color = if Some(*child_id) == self.selected_item {
                Some(ui.visuals().selection.bg_fill)
            } else {
                None
            };

            match self.items.get(child_id) {
                Some(Item::Container(children)) => {
                    self.container_ui(ui, *child_id, children);
                }
                Some(Item::Leaf(label)) => {
                    self.leaf_ui(ui, *child_id, label);
                }
                None => {}
            }
        }
    }

    fn leaf_ui(&self, ui: &mut egui::Ui, item_id: T, label: &str) {
        let text = egui::RichText::new(format!("{}", label)).size(14.);
        let response = ui.add(
            egui::Label::new(text)
                .selectable(false)
                .sense(egui::Sense::click_and_drag()),
        );

        if response.clicked() {
            self.send_command(Command::SetSelectedItem(Some(item_id)));
        }

        self.handle_drag_and_drop_interaction(ui, item_id, false, &response, None);
    }

    fn handle_drag_and_drop_interaction(
        &self,
        ui: &egui::Ui,
        item_id: T,
        is_container: bool,
        response: &egui::Response,
        body_response: Option<&egui::Response>,
    ) {
        //
        // handle start of drag
        //

        if response.drag_started() {
            egui::DragAndDrop::set_payload(ui.ctx(), item_id);

            // force selection to the dragged item
            self.send_command(Command::SetSelectedItem(Some(item_id)));
        }

        //
        // handle candidate drop
        //

        // find the item being dragged
        let Some(dragged_item_id) = egui::DragAndDrop::payload(ui.ctx()).map(|payload| (*payload))
        else {
            // nothing is being dragged, we're done here
            return;
        };

        ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing);

        let Some((parent_id, position_index_in_parent)) = self.parent_and_pos(item_id) else {
            // this shouldn't happen
            return;
        };

        let previous_container_id = if position_index_in_parent > 0 {
            self.container(parent_id)
                .map(|c| c[position_index_in_parent - 1])
                .filter(|id| self.container(*id).is_some())
        } else {
            None
        };

        let item_desc = super::drag_and_drop::DropItemDescription {
            id: item_id,
            is_container,
            parent_id,
            position_index_in_parent,
            previous_container_id,
        };

        //
        // compute the drag target areas based on the item and body responses
        //

        // adjust the drop target to account for the spacing between items
        let item_rect = response
            .rect
            .expand2(egui::Vec2::new(0.0, ui.spacing().item_spacing.y / 2.0));
        let body_rect = body_response.map(|r| {
            r.rect
                .expand2(egui::Vec2::new(0.0, ui.spacing().item_spacing.y))
        });

        //
        // find the candidate drop target
        //

        let drop_target = super::drag_and_drop::find_drop_target(
            ui,
            &item_desc,
            item_rect,
            body_rect,
            response.rect.height(),
        );

        if let Some(drop_target) = drop_target {
            // We cannot allow the target location to be "inside" the dragged item, because that would amount moving
            // myself inside of me.

            if self.contains(dragged_item_id, drop_target.target_parent_id) {
                return;
            }

            // extend the cursor to the right of the enclosing container
            let mut span_x = drop_target.indicator_span_x;
            span_x.max = ui.cursor().right();

            ui.painter().hline(
                span_x,
                drop_target.indicator_position_y,
                (1.0, egui::Color32::WHITE),
            );

            // note: can't use `response.drag_released()` because we not the item which
            // started the drag
            if ui.input(|i| i.pointer.any_released()) {
                self.send_command(Command::MoveItem {
                    moved_item_id: dragged_item_id,
                    target_container_id: drop_target.target_parent_id,
                    target_position_index: drop_target.target_position_index,
                });

                egui::DragAndDrop::clear_payload(ui.ctx());
            } else {
                self.send_command(Command::HighlightTargetContainer(
                    drop_target.target_parent_id,
                ));
            }
        }
    }
}
